// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "gpu/config/gpu_info_collector.h"

// This has to be included before windows.h.
#include "third_party/re2/re2/re2.h"

#include <cfgmgr32.h>
#include <d3d11.h>
#include <d3d9.h>
#include <dxgi.h>
#include <setupapi.h>
#include <windows.h>

#include "base/command_line.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/histogram.h"
#include "base/scoped_native_library.h"
#include "base/strings/string16.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread.h"
#include "base/threading/worker_pool.h"
#include "base/trace_event/trace_event.h"
#include "base/win/registry.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/scoped_comptr.h"
#include "base/win/windows_version.h"
#include "third_party/libxml/chromium/libxml_utils.h"
#include "ui/gl/gl_implementation.h"
#include "ui/gl/gl_surface_egl.h"

namespace gpu {

namespace {

    // This must be kept in sync with histograms.xml.
    enum DisplayLinkInstallationStatus {
        DISPLAY_LINK_NOT_INSTALLED,
        DISPLAY_LINK_7_1_OR_EARLIER,
        DISPLAY_LINK_7_2_OR_LATER,
        DISPLAY_LINK_INSTALLATION_STATUS_MAX
    };

    // Returns the display link driver version or an invalid version if it is
    // not installed.
    Version DisplayLinkVersion()
    {
        base::win::RegKey key;

        if (key.Open(HKEY_LOCAL_MACHINE, L"SOFTWARE", KEY_READ | KEY_WOW64_64KEY))
            return Version();

        if (key.OpenKey(L"DisplayLink", KEY_READ | KEY_WOW64_64KEY))
            return Version();

        if (key.OpenKey(L"Core", KEY_READ | KEY_WOW64_64KEY))
            return Version();

        base::string16 version;
        if (key.ReadValue(L"Version", &version))
            return Version();

        return Version(base::UTF16ToASCII(version));
    }

    // Returns whether Lenovo dCute is installed.
    bool IsLenovoDCuteInstalled()
    {
        base::win::RegKey key;

        if (key.Open(HKEY_LOCAL_MACHINE, L"SOFTWARE", KEY_READ | KEY_WOW64_64KEY))
            return false;

        if (key.OpenKey(L"Lenovo", KEY_READ | KEY_WOW64_64KEY))
            return false;

        if (key.OpenKey(L"Lenovo dCute", KEY_READ | KEY_WOW64_64KEY))
            return false;

        return true;
    }

    void DeviceIDToVendorAndDevice(const std::wstring& id,
        uint32* vendor_id,
        uint32* device_id)
    {
        *vendor_id = 0;
        *device_id = 0;
        if (id.length() < 21)
            return;
        base::string16 vendor_id_string = id.substr(8, 4);
        base::string16 device_id_string = id.substr(17, 4);
        int vendor = 0;
        int device = 0;
        base::HexStringToInt(base::UTF16ToASCII(vendor_id_string), &vendor);
        base::HexStringToInt(base::UTF16ToASCII(device_id_string), &device);
        *vendor_id = vendor;
        *device_id = device;
    }

} // namespace anonymous

#if defined(GOOGLE_CHROME_BUILD) && defined(OFFICIAL_BUILD)
// This function has a real implementation for official builds that can
// be found in src/third_party/amd.
void GetAMDVideocardInfo(GPUInfo* gpu_info);
#else
void GetAMDVideocardInfo(GPUInfo* gpu_info)
{
    DCHECK(gpu_info);
    return;
}
#endif

CollectInfoResult CollectDriverInfoD3D(const std::wstring& device_id,
    GPUInfo* gpu_info)
{
    TRACE_EVENT0("gpu", "CollectDriverInfoD3D");

    // Display adapter class GUID from
    // https://msdn.microsoft.com/en-us/library/windows/hardware/ff553426%28v=vs.85%29.aspx
    GUID display_class = { 0x4d36e968,
        0xe325,
        0x11ce,
        { 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 } };

    // create device info for the display device
    HDEVINFO device_info;
    if (base::win::GetVersion() <= base::win::VERSION_XP) {
        // Collection of information on all adapters is much slower on XP (almost
        // 100ms), and not very useful (as it's not going to use the GPU anyway), so
        // just collect information on the current device. http://crbug.com/456178
        device_info = SetupDiGetClassDevsW(NULL, device_id.c_str(), NULL,
            DIGCF_PRESENT | DIGCF_PROFILE | DIGCF_ALLCLASSES);
    } else {
        device_info = SetupDiGetClassDevsW(&display_class, NULL, NULL, DIGCF_PRESENT);
    }
    if (device_info == INVALID_HANDLE_VALUE) {
        LOG(ERROR) << "Creating device info failed";
        return kCollectInfoNonFatalFailure;
    }

    struct GPUDriver {
        GPUInfo::GPUDevice device;
        std::string driver_vendor;
        std::string driver_version;
        std::string driver_date;
    };

    std::vector<GPUDriver> drivers;

    int primary_device = -1;
    bool found_amd = false;
    bool found_intel = false;

    DWORD index = 0;
    SP_DEVINFO_DATA device_info_data;
    device_info_data.cbSize = sizeof(device_info_data);
    while (SetupDiEnumDeviceInfo(device_info, index++, &device_info_data)) {
        WCHAR value[255];
        if (SetupDiGetDeviceRegistryPropertyW(device_info,
                &device_info_data,
                SPDRP_DRIVER,
                NULL,
                reinterpret_cast<PBYTE>(value),
                sizeof(value),
                NULL)) {
            HKEY key;
            std::wstring driver_key = L"System\\CurrentControlSet\\Control\\Class\\";
            driver_key += value;
            LONG result = RegOpenKeyExW(
                HKEY_LOCAL_MACHINE, driver_key.c_str(), 0, KEY_QUERY_VALUE, &key);
            if (result == ERROR_SUCCESS) {
                DWORD dwcb_data = sizeof(value);
                std::string driver_version;
                result = RegQueryValueExW(
                    key, L"DriverVersion", NULL, NULL,
                    reinterpret_cast<LPBYTE>(value), &dwcb_data);
                if (result == ERROR_SUCCESS)
                    driver_version = base::UTF16ToASCII(std::wstring(value));

                std::string driver_date;
                dwcb_data = sizeof(value);
                result = RegQueryValueExW(
                    key, L"DriverDate", NULL, NULL,
                    reinterpret_cast<LPBYTE>(value), &dwcb_data);
                if (result == ERROR_SUCCESS)
                    driver_date = base::UTF16ToASCII(std::wstring(value));

                std::string driver_vendor;
                dwcb_data = sizeof(value);
                result = RegQueryValueExW(
                    key, L"ProviderName", NULL, NULL,
                    reinterpret_cast<LPBYTE>(value), &dwcb_data);
                if (result == ERROR_SUCCESS)
                    driver_vendor = base::UTF16ToASCII(std::wstring(value));

                wchar_t new_device_id[MAX_DEVICE_ID_LEN];
                CONFIGRET status = CM_Get_Device_ID(
                    device_info_data.DevInst, new_device_id, MAX_DEVICE_ID_LEN, 0);

                if (status == CR_SUCCESS) {
                    GPUDriver driver;

                    driver.driver_vendor = driver_vendor;
                    driver.driver_version = driver_version;
                    driver.driver_date = driver_date;
                    std::wstring id = new_device_id;

                    if (id.compare(0, device_id.size(), device_id) == 0)
                        primary_device = drivers.size();

                    uint32 vendor_id = 0, device_id = 0;
                    DeviceIDToVendorAndDevice(id, &vendor_id, &device_id);
                    driver.device.vendor_id = vendor_id;
                    driver.device.device_id = device_id;
                    drivers.push_back(driver);

                    if (vendor_id == 0x8086)
                        found_intel = true;
                    if (vendor_id == 0x1002)
                        found_amd = true;
                }

                RegCloseKey(key);
            }
        }
    }
    SetupDiDestroyDeviceInfoList(device_info);
    bool found = false;
    if (found_amd && found_intel) {
        // AMD Switchable system found.
        for (const auto& driver : drivers) {
            if (driver.device.vendor_id == 0x8086) {
                gpu_info->gpu = driver.device;
            }

            if (driver.device.vendor_id == 0x1002) {
                gpu_info->driver_vendor = driver.driver_vendor;
                gpu_info->driver_version = driver.driver_version;
                gpu_info->driver_date = driver.driver_date;
            }
        }
        GetAMDVideocardInfo(gpu_info);

        if (!gpu_info->amd_switchable) {
            // Some machines aren't properly detected as AMD switchable, but count
            // them anyway.
            gpu_info->amd_switchable = true;
            for (const auto& driver : drivers) {
                if (driver.device.vendor_id == 0x1002) {
                    gpu_info->gpu = driver.device;
                } else {
                    gpu_info->secondary_gpus.push_back(driver.device);
                }
            }
        }
        found = true;
    } else {
        for (size_t i = 0; i < drivers.size(); ++i) {
            const GPUDriver& driver = drivers[i];
            if (static_cast<int>(i) == primary_device) {
                found = true;
                gpu_info->gpu = driver.device;
                gpu_info->driver_vendor = driver.driver_vendor;
                gpu_info->driver_version = driver.driver_version;
                gpu_info->driver_date = driver.driver_date;
            } else {
                gpu_info->secondary_gpus.push_back(driver.device);
            }
        }
    }

    return found ? kCollectInfoSuccess : kCollectInfoNonFatalFailure;
}

CollectInfoResult CollectContextGraphicsInfo(GPUInfo* gpu_info)
{
    TRACE_EVENT0("gpu", "CollectGraphicsInfo");

    DCHECK(gpu_info);

    if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kUseGL)) {
        std::string requested_implementation_name = base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
            switches::kUseGL);
        if (requested_implementation_name == "swiftshader") {
            gpu_info->software_rendering = true;
            gpu_info->context_info_state = kCollectInfoNonFatalFailure;
            return kCollectInfoNonFatalFailure;
        }
    }

    CollectInfoResult result = CollectGraphicsInfoGL(gpu_info);
    if (result != kCollectInfoSuccess) {
        gpu_info->context_info_state = result;
        return result;
    }

    // ANGLE's renderer strings are of the form:
    // ANGLE (<adapter_identifier> Direct3D<version> vs_x_x ps_x_x)
    std::string direct3d_version;
    int vertex_shader_major_version = 0;
    int vertex_shader_minor_version = 0;
    int pixel_shader_major_version = 0;
    int pixel_shader_minor_version = 0;
    gpu_info->adapter_luid = 0;
    if (RE2::FullMatch(gpu_info->gl_renderer,
            "ANGLE \\(.*\\)")
        && RE2::PartialMatch(gpu_info->gl_renderer,
            " Direct3D(\\w+)",
            &direct3d_version)
        && RE2::PartialMatch(gpu_info->gl_renderer,
            " vs_(\\d+)_(\\d+)",
            &vertex_shader_major_version,
            &vertex_shader_minor_version)
        && RE2::PartialMatch(gpu_info->gl_renderer,
            " ps_(\\d+)_(\\d+)",
            &pixel_shader_major_version,
            &pixel_shader_minor_version)) {
        gpu_info->can_lose_context = direct3d_version == "9";
        gpu_info->vertex_shader_version = base::StringPrintf("%d.%d",
            vertex_shader_major_version,
            vertex_shader_minor_version);
        gpu_info->pixel_shader_version = base::StringPrintf("%d.%d",
            pixel_shader_major_version,
            pixel_shader_minor_version);

        // ANGLE's EGL vendor strings are of the form:
        // Google, Inc. (adapter LUID: 0123456789ABCDEF)
        // The LUID is optional and identifies the GPU adapter ANGLE is using.
        const char* egl_vendor = eglQueryString(
            gfx::GLSurfaceEGL::GetHardwareDisplay(),
            EGL_VENDOR);
        RE2::PartialMatch(egl_vendor,
            " \\(adapter LUID: ([0-9A-Fa-f]{16})\\)",
            RE2::Hex(&gpu_info->adapter_luid));

        // DirectX diagnostics are collected asynchronously because it takes a
        // couple of seconds.
    } else {
        gpu_info->dx_diagnostics_info_state = kCollectInfoNonFatalFailure;
    }

    gpu_info->context_info_state = kCollectInfoSuccess;
    return kCollectInfoSuccess;
}

CollectInfoResult CollectGpuID(uint32* vendor_id, uint32* device_id)
{
    DCHECK(vendor_id && device_id);
    *vendor_id = 0;
    *device_id = 0;

    // Taken from http://developer.nvidia.com/object/device_ids.html
    DISPLAY_DEVICE dd;
    dd.cb = sizeof(DISPLAY_DEVICE);
    std::wstring id;
    for (int i = 0; EnumDisplayDevices(NULL, i, &dd, 0); ++i) {
        if (dd.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) {
            id = dd.DeviceID;
            break;
        }
    }

    if (id.length() > 20) {
        DeviceIDToVendorAndDevice(id, vendor_id, device_id);
        if (*vendor_id != 0 && *device_id != 0)
            return kCollectInfoSuccess;
    }
    return kCollectInfoNonFatalFailure;
}

CollectInfoResult CollectBasicGraphicsInfo(GPUInfo* gpu_info)
{
    TRACE_EVENT0("gpu", "CollectPreliminaryGraphicsInfo");

    DCHECK(gpu_info);

    // nvd3d9wrap.dll is loaded into all processes when Optimus is enabled.
    HMODULE nvd3d9wrap = GetModuleHandleW(L"nvd3d9wrap.dll");
    gpu_info->optimus = nvd3d9wrap != NULL;

    gpu_info->lenovo_dcute = IsLenovoDCuteInstalled();

    gpu_info->display_link_version = DisplayLinkVersion();

    if (!gpu_info->display_link_version.IsValid()) {
        UMA_HISTOGRAM_ENUMERATION("GPU.DisplayLinkInstallationStatus",
            DISPLAY_LINK_NOT_INSTALLED,
            DISPLAY_LINK_INSTALLATION_STATUS_MAX);
    } else if (gpu_info->display_link_version.IsOlderThan("7.2")) {
        UMA_HISTOGRAM_ENUMERATION("GPU.DisplayLinkInstallationStatus",
            DISPLAY_LINK_7_1_OR_EARLIER,
            DISPLAY_LINK_INSTALLATION_STATUS_MAX);
    } else {
        UMA_HISTOGRAM_ENUMERATION("GPU.DisplayLinkInstallationStatus",
            DISPLAY_LINK_7_2_OR_LATER,
            DISPLAY_LINK_INSTALLATION_STATUS_MAX);
    }

    // Taken from http://developer.nvidia.com/object/device_ids.html
    DISPLAY_DEVICE dd;
    dd.cb = sizeof(DISPLAY_DEVICE);
    std::wstring id;
    for (int i = 0; EnumDisplayDevices(NULL, i, &dd, 0); ++i) {
        if (dd.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) {
            id = dd.DeviceID;
            break;
        }
    }

    if (id.length() <= 20) {
        gpu_info->basic_info_state = kCollectInfoNonFatalFailure;
        return kCollectInfoNonFatalFailure;
    }

    DeviceIDToVendorAndDevice(id, &gpu_info->gpu.vendor_id,
        &gpu_info->gpu.device_id);
    // TODO(zmo): we only need to call CollectDriverInfoD3D() if we use ANGLE.
    if (!CollectDriverInfoD3D(id, gpu_info)) {
        gpu_info->basic_info_state = kCollectInfoNonFatalFailure;
        return kCollectInfoNonFatalFailure;
    }

    gpu_info->basic_info_state = kCollectInfoSuccess;
    return kCollectInfoSuccess;
}

CollectInfoResult CollectDriverInfoGL(GPUInfo* gpu_info)
{
    TRACE_EVENT0("gpu", "CollectDriverInfoGL");

    if (!gpu_info->driver_version.empty())
        return kCollectInfoSuccess;

    bool parsed = RE2::PartialMatch(
        gpu_info->gl_version, "([\\d\\.]+)$", &gpu_info->driver_version);
    return parsed ? kCollectInfoSuccess : kCollectInfoNonFatalFailure;
}

void MergeGPUInfo(GPUInfo* basic_gpu_info,
    const GPUInfo& context_gpu_info)
{
    DCHECK(basic_gpu_info);

    if (context_gpu_info.software_rendering) {
        basic_gpu_info->software_rendering = true;
        return;
    }

    // Track D3D Shader Model (if available)
    const std::string& shader_version = context_gpu_info.vertex_shader_version;

    // Only gather if this is the first time we're seeing
    // a non-empty shader version string.
    if (!shader_version.empty() && basic_gpu_info->vertex_shader_version.empty()) {

        // Note: do not reorder, used by UMA_HISTOGRAM below
        enum ShaderModel {
            SHADER_MODEL_UNKNOWN,
            SHADER_MODEL_2_0,
            SHADER_MODEL_3_0,
            SHADER_MODEL_4_0,
            SHADER_MODEL_4_1,
            SHADER_MODEL_5_0,
            NUM_SHADER_MODELS
        };

        ShaderModel shader_model = SHADER_MODEL_UNKNOWN;

        if (shader_version == "5.0") {
            shader_model = SHADER_MODEL_5_0;
        } else if (shader_version == "4.1") {
            shader_model = SHADER_MODEL_4_1;
        } else if (shader_version == "4.0") {
            shader_model = SHADER_MODEL_4_0;
        } else if (shader_version == "3.0") {
            shader_model = SHADER_MODEL_3_0;
        } else if (shader_version == "2.0") {
            shader_model = SHADER_MODEL_2_0;
        }

        UMA_HISTOGRAM_ENUMERATION("GPU.D3DShaderModel",
            shader_model,
            NUM_SHADER_MODELS);
    }

    MergeGPUInfoGL(basic_gpu_info, context_gpu_info);

    basic_gpu_info->dx_diagnostics_info_state = context_gpu_info.dx_diagnostics_info_state;
    basic_gpu_info->dx_diagnostics = context_gpu_info.dx_diagnostics;
}

} // namespace gpu
